spb/groupe-ka Public
Groupe KA — site du holding + KA ID (compte unique & SSO des 7 plateformes). Next.js 16, SQLite, Google & Apple login.
TypeScript 89.7%
CSS 5.8%
HTML 4.4%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// Page d'identification d'un membre (cible du code QR de la carte) :3// grande photo, identité complète, badge de vérification. Le courriel et4// les coordonnées ne sont jamais exposés.5import type { Metadata } from "next";6import { db } from "@/lib/db";7import { roleLabel } from "@/lib/roles";89export const metadata: Metadata = {10 title: "Identification de membre | Groupe KA",11 description: "Identification d'un membre du Groupe KA par son KA-ID.",12 robots: { index: false },13};1415export const dynamic = "force-dynamic";1617function fmtDate(iso: string | null): string {18 if (!iso) return "—";19 const d = new Date(iso.includes("T") ? iso : iso.replace(" ", "T") + "Z");20 if (Number.isNaN(d.getTime())) return "—";21 return d22 .toLocaleDateString("fr-CA", { day: "numeric", month: "long", year: "numeric" })23 .replace(/^1 /, "1ᵉʳ ");24}2526export default async function IdentificationMembre({27 params,28}: {29 params: Promise<{ kaId: string }>;30}) {31 const { kaId } = await params;32 const clean = /^ka-\d{10}$/.test(kaId) ? kaId : null;33 const row = clean34 ? (db35 .prepare(36 "SELECT ka_id, name, avatar_url, google_sub, role, created_at, last_login FROM users WHERE ka_id = ?",37 )38 .get(clean) as39 | {40 ka_id: string;41 name: string;42 avatar_url: string | null;43 google_sub: string | null;44 role: string | null;45 created_at: string;46 last_login: string | null;47 }48 | undefined)49 : undefined;5051 if (!row) {52 return (53 <main className="mx-auto max-w-md px-4 pb-16 sm:px-6">54 <section className="pt-12 text-center sm:pt-14">55 <p className="kicker rise justify-center">Identification de membre</p>56 <h1 className="gk-display rise mt-4 text-[clamp(28px,5vw,40px)] leading-[1.02] font-bold tracking-[-0.035em] uppercase [animation-delay:0.08s]">57 Identifiant <span className="outline-txt">introuvable</span>58 </h1>59 <p className="rise mt-4 text-[14px] text-ink-2 [animation-delay:0.16s]">60 Aucun membre ne porte cet identifiant. Vérifiez le code, ou{" "}61 <a href="/connexion" className="underline underline-offset-4">62 créez votre KA ID63 </a>64 .65 </p>66 </section>67 </main>68 );69 }7071 const infos: [string, string][] = [72 ["Identifiant membre", row.ka_id],73 ...(roleLabel(row.role)74 ? ([["Statut", roleLabel(row.role)!]] as [string, string][])75 : []),76 ["Membre depuis", fmtDate(row.created_at)],77 ["Dernière connexion", fmtDate(row.last_login)],78 ["Compte", row.google_sub ? "Lié à Google" : "Courriel vérifié"],79 ["Valide sur", "Les 7 plateformes ·Ka"],80 ];8182 return (83 <main className="mx-auto max-w-md px-4 pb-16 sm:px-6">84 <section className="pt-10 text-center sm:pt-12">85 <p className="kicker rise justify-center">Identification de membre</p>8687 {/* ——— La grande photo ——— */}88 <div className="rise mt-7 flex justify-center [animation-delay:0.08s]">89 {row.avatar_url ? (90 // eslint-disable-next-line @next/next/no-img-element91 <img92 src={row.avatar_url}93 alt={`Photo de ${row.name}`}94 width={176}95 height={176}96 referrerPolicy="no-referrer"97 className="h-44 w-44 rounded-full border-[3px] border-ink object-cover shadow-[8px_8px_0_rgba(20,24,20,0.18)]"98 />99 ) : (100 <span className="gk-display flex h-44 w-44 items-center justify-center rounded-full border-[3px] border-ink bg-lime text-[64px] font-bold shadow-[8px_8px_0_rgba(20,24,20,0.18)]">101 {row.name.charAt(0).toUpperCase()}102 </span>103 )}104 </div>105106 <h1 className="gk-display rise mt-6 text-[clamp(28px,6vw,42px)] leading-[1.02] font-bold tracking-[-0.03em] [animation-delay:0.14s]">107 {row.name}108 </h1>109 <p className="rise mt-3 inline-flex items-center gap-2 rounded-full border-[1.5px] border-ink bg-lime px-4 py-[7px] text-[13px] font-bold [animation-delay:0.2s]">110 <span111 aria-hidden="true"112 className="flex h-[18px] w-[18px] items-center justify-center rounded-full bg-ink text-[11px] font-bold text-lime"113 >114 ✓115 </span>116 Membre vérifié du Groupe KA117 </p>118119 {/* ——— KA-ID ——— */}120 <div className="gk-card rise mt-7 !bg-ink p-6 text-paper [animation-delay:0.26s]">121 <p className="gk-mono text-[10px] tracking-[0.2em] uppercase text-[rgba(217,242,107,0.65)]">122 KA-ID123 </p>124 <p className="gk-mono mt-2 text-[clamp(22px,6vw,30px)] font-bold tracking-[0.14em] text-lime [text-shadow:0_0_24px_rgba(217,242,107,0.35)]">125 {row.ka_id}126 </p>127 </div>128129 {/* ——— Toutes les infos ——— */}130 <div className="gk-card rise mt-5 p-0 text-left [animation-delay:0.32s]">131 {infos.map(([label, value], i) => (132 <div133 key={label}134 className={`flex items-baseline justify-between gap-4 px-5 py-[13px] ${i < infos.length - 1 ? "border-b border-dashed border-[rgba(20,24,20,0.18)]" : ""}`}135 >136 <span className="klabel flex-none">{label}</span>137 <span138 className={`text-right text-[13.5px] font-semibold ${label === "Identifiant membre" ? "gk-mono text-green" : ""}`}139 >140 {value}141 </span>142 </div>143 ))}144 </div>145146 <p className="mt-5 text-[12.5px] text-ink-2">147 Cette page confirme l'identité d'un membre à partir du code148 QR de sa carte. Le courriel et les coordonnées ne sont jamais149 affichés.150 </p>151 <a href="/" className="btn btn-ghost mt-4">152 groupe-ka.com ↗153 </a>154 </section>155 </main>156 );157}158